﻿// <copyright file="CredentialHelper.cs" company="Soumya Chattopadhyay">
// Copyright (c) 2010 All Right Reserved
// </copyright>
// <disclaimer> This software is intented for educational purposes only and not for 
// commerical or prodution use. The software is provided "as is", without warranty 
// of any kind, express or implied, including but not limited to the warranties of 
// merchantability, fitness for a particular purpose and noninfringement. In no event 
// shall the author(s) or copyright holder(s) be liable for any claim, damages or other 
// liability, whether in an action of contract, tort or otherwise, arising from, out of 
// or in connection with the software or the use or other dealings in the software.
// </disclaimer>

namespace My.CodePlex.TFSExplorer
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Runtime.InteropServices;
    using System.Text;
    using Microsoft.TeamFoundation.Client;

    /// <summary>
    /// The CredUI flags
    /// </summary>
    [Flags]
    internal enum CREDUI_FLAGS
    {
        /// <summary>
        /// Notify the user of insufficient credentials by displaying the "Logon unsuccessful" balloon tip.
        /// </summary>
        INCORRECT_PASSWORD = 0x1,

        /// <summary>
        /// Do not store credentials or display check boxes. You can pass CREDUI_FLAGS_SHOW_SAVE_CHECK_BOX with this flag to display the Save check box only, and the result is returned in the pfSave output parameter.
        /// </summary>
        DO_NOT_PERSIST = 0x2,

        /// <summary>
        /// Populate the combo box with local administrators only. 
        /// Windows XP Home Edition:  This flag will filter out the well-known Administrator account.
        /// </summary>
        REQUEST_ADMINISTRATOR = 0x4,

        /// <summary>
        /// Populate the combo box with user name/password only. Do not display certificates or smart cards in the combo box.
        /// </summary>
        EXCLUDE_CERTIFICATES = 0x8,

        /// <summary>
        /// Populate the combo box with certificates and smart cards only. Do not allow a user name to be entered.
        /// </summary>
        REQUIRE_CERTIFICATE = 0x10,

        /// <summary>
        /// If the check box is selected, show the Save check box and return TRUE in the pfSave output parameter, otherwise, return FALSE. CREDUI_FLAGS_DO_NOT_PERSIST must be specified to use this flag. Check box uses the value in pfSave by default.
        /// </summary>
        SHOW_SAVE_CHECK_BOX = 0x40,

        /// <summary>
        /// Specifies that a user interface will be shown even if the credentials can be returned from an existing credential in credential manager. This flag is permitted only if CREDUI_FLAGS_GENERIC_CREDENTIALS is also specified.
        /// </summary>
        ALWAYS_SHOW_UI = 0x80,

        /// <summary>
        /// Populate the combo box with certificates or smart cards only. Do not allow a user name to be entered.
        /// </summary>
        REQUIRE_SMARTCARD = 0x100,

        /// <summary>
        /// Not documented.
        /// </summary>
        PASSWORD_ONLY_OK = 0x200,

        /// <summary>
        /// Not documented.
        /// </summary>
        VALIDATE_USERNAME = 0x400,

        /// <summary>
        /// Not documented.
        /// </summary>
        COMPLETE_USERNAME = 0x800,

        /// <summary>
        /// Do not show the Save check box, but the credential is saved as though the box were shown and selected.
        /// </summary>
        PERSIST = 0x1000,

        /// <summary>
        /// This flag is meaningful only in locating a matching credential to prefill the dialog box, should authentication fail. When this flag is specified, wildcard credentials will not be matched. It has no effect when writing a credential. CredUI does not create credentials that contain wildcard characters. Any found were either created explicitly by the user or created programmatically, as happens when a RAS connection is made.
        /// </summary>
        SERVER_CREDENTIAL = 0x4000,

        /// <summary>
        /// Specifies that the caller will call CredUIConfirmCredentials after checking to determine whether the returned credentials are actually valid. This mechanism ensures that invalid credentials are not saved to the credential manager. Specify this flag in all cases unless CREDUI_FLAGS_DO_NOT_PERSIST is specified.
        /// </summary>
        EXPECT_CONFIRMATION = 0x20000,

        /// <summary>
        /// Consider the credentials entered by the user to be generic credentials.
        /// </summary>
        GENERIC_CREDENTIALS = 0x40000,

        /// <summary>
        /// The credential is a "runas" credential. The TargetName parameter specifies the name of the command or program being run. It is used for prompting purposes only.
        /// </summary>
        USERNAME_TARGET_CREDENTIALS = 0x80000,

        /// <summary>
        /// Not documented.
        /// </summary>
        KEEP_USERNAME = 0x100000
    }

    /// <summary>
    /// CredUI return codes
    /// </summary>
    internal enum CredUIReturnCodes
    {
        /// <summary>
        /// User chose OK.
        /// </summary>
        NO_ERROR = 0,

        /// <summary>
        /// User chose Cancel. 
        /// </summary>
        ERROR_CANCELLED = 1223,

        /// <summary>
        /// The credential manager cannot be used.
        /// </summary>
        ERROR_NO_SUCH_LOGON_SESSION = 1312,

        /// <summary>
        /// Not documented.
        /// </summary>
        ERROR_NOT_FOUND = 1168,

        /// <summary>
        /// Invalid account name
        /// </summary>
        ERROR_INVALID_ACCOUNT_NAME = 1315,

        /// <summary>
        /// Insufficient buffer size
        /// </summary>
        ERROR_INSUFFICIENT_BUFFER = 122,

        /// <summary>
        /// Either pszTargetName is NULL, the empty string, or longer than CREDUI_MAX_DOMAIN_LENGTH, or pUiInfo is not NULL etc.
        /// </summary>
        ERROR_INVALID_PARAMETER = 87,

        /// <summary>
        /// This status is returned for any of the flag configurations that are not valid.
        /// </summary>
        ERROR_INVALID_FLAGS = 1004
    }

    /// <summary>
    /// The CREDUI_INFO struct
    /// </summary>
    internal struct CREDUI_INFO
    {
        /// <summary>
        /// Set to the size of the CREDUI_INFO structure.
        /// </summary>
        public int Cbsize;

        /// <summary>
        /// Specifies the handle to the parent window of the dialog box.
        /// </summary>
        public IntPtr Hwndparent;

        /// <summary>
        /// Pointer to a string containing a brief message to display in the dialog box.
        /// </summary>
        [MarshalAs(UnmanagedType.LPWStr)]
        public string PszmessageText;

        /// <summary>
        /// Pointer to a string containing the title for the dialog box.
        /// </summary>
        [MarshalAs(UnmanagedType.LPWStr)]
        public string PszcaptionText;

        /// <summary>
        /// Bitmap to display in the dialog box.
        /// </summary>
        public IntPtr Hbmbanner;
    }

    /// <summary>
    /// The credential helper class
    /// </summary>
    public static class CredentialHelper
    {
        #region Fields

        /// <summary>
        /// The max user name
        /// </summary>
        private const int MAXUSERNAME = 100;

        /// <summary>
        /// The max password
        /// </summary>
        private const int MAXPASSWORD = 100;

        /// <summary>
        /// The max domain name size
        /// </summary>
        private const int MAXDOMAIN = 100;

        #endregion

        /// <summary>
        /// Get the network credentials
        /// </summary>
        /// <param name="host">A pointer to a null-terminated string that contains the name of the target for the credentials, typically a server name.</param>
        /// <param name="description">The description</param>
        /// <param name="hwndParent">Handle to the parent window</param>
        /// <returns>The user credentials</returns>
        public static NetworkCredential GetCredentials(string host, string description, IntPtr hwndParent)
        {
            CREDUI_FLAGS flags = CREDUI_FLAGS.GENERIC_CREDENTIALS | CREDUI_FLAGS.DO_NOT_PERSIST;
            return GetCredentials(new Uri(host), description, hwndParent, flags, null);
        }

        /// <summary>
        /// Get the user credentials
        /// </summary>
        /// <param name="uri">A pointer to a null-terminated string that contains the name of the target for the credentials, typically a server name.</param>
        /// <param name="description">The description</param>
        /// <param name="hwndParent">Handle to the parent window</param>
        /// <param name="flags">A DWORD value that specifies special behavior for this function.</param>
        /// <param name="failedCredentials">Failed credentials</param>
        /// <returns>The user credential</returns>
        internal static NetworkCredential GetCredentials(Uri uri, string description, IntPtr hwndParent, CREDUI_FLAGS flags, System.Net.ICredentials failedCredentials)
        {
            string userid = string.Empty;
            string password = string.Empty;
            bool savePwd = false;

            if (failedCredentials != null)
            {
                NetworkCredential networkCredential = failedCredentials.GetCredential(uri, "NTLM");

                if (networkCredential != null)
                {
                    userid = string.IsNullOrEmpty(networkCredential.Domain) ? networkCredential.UserName : string.Format("{0}\\{1}", networkCredential.Domain, networkCredential.UserName);
                    password = networkCredential.Password;
                }
            }

            CREDUI_INFO credInfo = new CREDUI_INFO();
            credInfo.Hwndparent = hwndParent;
            credInfo.PszcaptionText = uri.Host;
            credInfo.PszmessageText = description;
            CredUIReturnCodes result;
            result = PromptForCredentials(ref credInfo, uri.Host, 0, ref userid, ref password, ref savePwd, flags);

            switch (result)
            {
                case CredUIReturnCodes.ERROR_INVALID_FLAGS:
                    {
                        throw new ArgumentException("The flags passed were an invalid combination", "flags");
                    }

                case CredUIReturnCodes.ERROR_CANCELLED:
                    {
                        return null;
                    }
            }

            string parsedUserid = string.Empty;
            string parsedDomain = string.Empty;
            ParseUserName(userid, ref parsedUserid, ref parsedDomain);
            return new NetworkCredential(parsedUserid, password, parsedDomain);
        }

        /// <summary>
        /// Wrapper for the CredUIPromptForCredentials call
        /// </summary>
        /// <param name="creditUI">A pointer to a CREDUI_INFO structure that contains information for customizing the appearance of the dialog box.</param>
        /// <param name="targetName">A pointer to a null-terminated string that contains the name of the target for the credentials, typically a server name.</param>
        /// <param name="netError">Specifies why the credential dialog box is needed.</param>
        /// <param name="userName">A pointer to a null-terminated string that contains the user name for the credentials.</param>
        /// <param name="password">A pointer to a null-terminated string that contains the password for the credentials.</param>
        /// <param name="save">A pointer to a BOOL that specifies the initial state of the Save check box and receives the state of the Save check box after the user has responded to the dialog box.</param>
        /// <param name="flags">A DWORD value that specifies special behavior for this function.</param>
        /// <returns>The error code</returns>
        internal static CredUIReturnCodes PromptForCredentials(ref CREDUI_INFO creditUI, string targetName, int netError, ref string userName, ref string password, ref bool save, CREDUI_FLAGS flags)
        {
            int saveCredentials;
            StringBuilder user = new StringBuilder(MAXUSERNAME);
            StringBuilder pwd = new StringBuilder(MAXPASSWORD);

            saveCredentials = Convert.ToInt32(save);
            creditUI.Cbsize = Marshal.SizeOf(creditUI);

            CredUIReturnCodes result = CredentialHelper.CredUIPromptForCredentials(ref creditUI, ref targetName, IntPtr.Zero, netError, user, MAXUSERNAME, pwd, MAXPASSWORD, ref saveCredentials, flags);
            save = Convert.ToBoolean(saveCredentials);
            userName = user.ToString();
            password = pwd.ToString();

            return result;
        }

        /// <summary>
        /// Parse the user name
        /// </summary>
        /// <param name="userName">The full user name (including domain)</param>
        /// <param name="userPart">The extracted user name</param>
        /// <param name="domainPart">The extracted domain</param>
        /// <returns>The error code</returns>
        internal static CredUIReturnCodes ParseUserName(string userName, ref string userPart, ref string domainPart)
        {
            StringBuilder user = new StringBuilder(MAXUSERNAME);
            StringBuilder domain = new StringBuilder(MAXDOMAIN);

            CredUIReturnCodes result = CredentialHelper.CredUIParseUserName(ref userName, user, MAXUSERNAME, domain, MAXDOMAIN);
            userPart = user.ToString();

            domainPart = domain.ToString();
            return result;
        }

        /// <summary>
        /// Confirm the user credentials
        /// </summary>
        /// <param name="uri">Target for the credentials, typically a domain or server</param>
        /// <returns>The error code</returns>
        internal static CredUIReturnCodes ConfirmCredentials(Uri uri)
        {
            return CredentialHelper.ConfirmCredentials(uri.Host, true);
        }

        /// <summary>
        /// Confirm the user credentials
        /// </summary>
        /// <param name="target">Target for the credentials, typically a domain or server.</param>
        /// <param name="confirm">True if confirmed</param>
        /// <returns>The error code</returns>
        internal static CredUIReturnCodes ConfirmCredentials(string target, bool confirm)
        {
            return CredentialHelper.CredUIConfirmCredentials(ref target, confirm);
        }

        /// <summary>
        /// The CredUIConfirmCredentials function is called after CredUIPromptForCredentials or CredUICmdLinePromptForCredentials, to confirm the validity of the credential harvested.
        /// </summary>
        /// <param name="targetName">Pointer to a null-terminated string that contains the name of the target for the credentials, typically a domain or server application name.</param>
        /// <param name="confirm">Specifies whether the credentials returned from the prompt function are valid.</param>
        /// <returns>Status of the operation is returned.</returns>
        [DllImport("credui", EntryPoint = "CredUIConfirmCredentialsW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
        private static extern CredUIReturnCodes CredUIConfirmCredentials([MarshalAs(UnmanagedType.VBByRefStr)] ref string targetName, bool confirm);

        /// <summary>
        /// The CredUIParseUserName function extracts the domain and user account name from a fully qualified user name.
        /// </summary>
        /// <param name="userName">Pointer to a null-terminated string that contains the user name to be parsed.</param>
        /// <param name="user">Pointer to a null-terminated string that receives the user account name.</param>
        /// <param name="userMaxChars">Maximum number of characters to write to the pszUser string including the terminating null character.</param>
        /// <param name="domain">Pointer to a null-terminated string that receives the domain name.</param>
        /// <param name="domainMaxChars">Maximum number of characters to write to the pszDomain string including the terminating null character.</param>
        /// <returns>•NO_ERROR if the user name is valid.</returns>
        [DllImport("credui", EntryPoint = "CredUIParseUserNameW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
        private static extern CredUIReturnCodes CredUIParseUserName([MarshalAs(UnmanagedType.VBByRefStr)] ref string userName, StringBuilder user, int userMaxChars, StringBuilder domain, int domainMaxChars);

        /// <summary>
        /// The CredUIPromptForCredentials function creates and displays a configurable dialog box that accepts credentials information from a user.
        /// </summary>
        /// <param name="creditUR">A pointer to a CREDUI_INFO structure that contains information for customizing the appearance of the dialog box.</param>
        /// <param name="targetName">A pointer to a null-terminated string that contains the name of the target for the credentials, typically a server name.</param>
        /// <param name="reserved1">This parameter is reserved for future use. It must be NULL.</param>
        /// <param name="ierror">Specifies why the credential dialog box is needed.</param>
        /// <param name="userName">A pointer to a null-terminated string that contains the user name for the credentials.</param>
        /// <param name="maxUserName">The maximum number of characters that can be copied to pszUserName including the terminating null character.</param>
        /// <param name="password">A pointer to a null-terminated string that contains the password for the credentials.</param>
        /// <param name="maxPassword">The maximum number of characters that can be copied to pszPassword including the terminating null character.</param>
        /// <param name="isave">A pointer to a BOOL that specifies the initial state of the Save check box and receives the state of the Save check box after the user has responded to the dialog box.</param>
        /// <param name="flags">A DWORD value that specifies special behavior for this function.</param>
        /// <returns>The return value is a DWORD.</returns>
        [DllImport("credui", EntryPoint = "CredUIPromptForCredentialsW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
        private static extern CredUIReturnCodes CredUIPromptForCredentials(ref CREDUI_INFO creditUR, [MarshalAs(UnmanagedType.VBByRefStr)] ref string targetName, IntPtr reserved1, int ierror, StringBuilder userName, int maxUserName, StringBuilder password, int maxPassword, ref int isave, CREDUI_FLAGS flags);
    }
}
